package com.canyou.basic.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import com.aliyun.oss.model.PutObjectResult;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.canyou.basic.entity.media.MediaVO;
import com.canyou.basic.mapper.MediaMapper;
import com.canyou.basic.model.Media;
import com.canyou.basic.service.MediaService;
import com.canyou.constants.CommonConstants;
import com.canyou.enums.ErrorCodeEnum;
import com.canyou.enums.MediaAccessTypeEnum;
import com.canyou.enums.MediaStoreTypeEnum;
import com.canyou.exception.CysBusinessException;
import com.canyou.oss.*;
import com.canyou.utils.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiniu.storage.model.DefaultPutRet;
import io.minio.ObjectWriteResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.canyou.api.ResponseBO.illegal;

/**
 * <p>
 * 上传文件资源表 服务实现类
 * </p>
 *
 * @author canyou
 * @since 2020-02-13
 */
@Service
@Slf4j
public class MediaServiceImpl extends ServiceImpl<MediaMapper, Media> implements MediaService {

    @Autowired
    private MultipartProperties multipartProperties;

    @Autowired
    private LocalUploadProperties uploadProperties;

    @Autowired
    private QiNiuOssProperties qiNiuOssProperties;

    @Autowired
    private AliOssProperties aliOssProperties;

    @Autowired
    private MinioOssProperties minioOssProperties;

    @Value("${springfox.documentation.swagger.v2.host}")
    private String apiHost;

    @Autowired
    private QiNiuOssService qiNiuOssService;

    @Autowired
    private AliOssService aliOssService;

    @Autowired
    private MinioOssService minioOssService;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public String getMediaPath(final HttpServletRequest request, Long mediaId) {
        final String rootUrl = RequestUtils.rootUrl(request);
        final String apiContextPath = StringUtils.isNotBlank(apiHost) ? apiHost : "";
        return rootUrl + apiContextPath + CommonConstants.DOWN_LOAD_PATH + mediaId;
    }

    @Override
    public String getMediaUrl(final HttpServletRequest request, Long mediaId) {
        return String.format("%s?%s=%s", getMediaPath(request, mediaId),
                CommonConstants.ACCESS_TOKEN_CODE,
                Oauth2Utils.getToken());
    }

    @Override
    public void getMediaPath(final HttpServletRequest request, Media media) {
        MediaStoreTypeEnum storeTypeEnum = MediaStoreTypeEnum.valueOf(media.getStoreType());
        if (MediaStoreTypeEnum.LOCAL.equals(storeTypeEnum)) {
            media.setMediaPath(getMediaPath(request, media.getId()));
        } else if (MediaStoreTypeEnum.ALI.equals(storeTypeEnum)) {
            MediaAccessTypeEnum accessTypeEnum = MediaAccessTypeEnum.valueOf(media.getAccessType());
            if (MediaAccessTypeEnum.PRIVATE.equals(accessTypeEnum)) {
                media.setMediaPath(aliOssService.privateAccessUrl(media.getMediaName()));
            } else {
                media.setMediaPath(aliOssService.publicAccessUrl(media.getMediaName()));
            }
        }
    }

    @Override
    public void getMediaUrl(final HttpServletRequest request, Media media) {
        MediaStoreTypeEnum storeTypeEnum = MediaStoreTypeEnum.valueOf(media.getStoreType());
        if (MediaStoreTypeEnum.LOCAL.equals(storeTypeEnum)) {
            media.setMediaPath(getMediaUrl(request, media.getId()));
        } else if (MediaStoreTypeEnum.ALI.equals(storeTypeEnum)) {
            MediaAccessTypeEnum accessTypeEnum = MediaAccessTypeEnum.valueOf(media.getAccessType());
            if (MediaAccessTypeEnum.PRIVATE.equals(accessTypeEnum)) {
                media.setMediaPath(aliOssService.privateAccessUrl(media.getMediaName()));
            } else {
                media.setMediaPath(aliOssService.publicAccessUrl(media.getMediaName()));
            }
        }
    }

    @Override
    public String getMediaUrl(final HttpServletRequest request, String mediaId) {
        Long id = Long.valueOf(mediaId);
        Media media = getById(id);
        if (media == null) {
            return null;
        }
        MediaStoreTypeEnum storeTypeEnum = MediaStoreTypeEnum.valueOf(media.getStoreType());
        if (MediaStoreTypeEnum.LOCAL.equals(storeTypeEnum)) {
            return getMediaPath(request, media.getId());
        } else if (MediaStoreTypeEnum.ALI.equals(storeTypeEnum)) {
            MediaAccessTypeEnum accessTypeEnum = MediaAccessTypeEnum.valueOf(media.getAccessType());
            if (MediaAccessTypeEnum.PRIVATE.equals(accessTypeEnum)) {
                return aliOssService.privateAccessUrl(media.getMediaName());
            } else {
                return aliOssService.publicAccessUrl(media.getMediaName());
            }
        } else {
            return media.getMediaPath();
        }
    }

    /**
     * 生成 MediaVO 的name, mediaPath, url 信息
     *
     * @param request
     * @param media
     */
    private void genMediaVo(final HttpServletRequest request, MediaVO media) {
        // 1.本地存储,2.七牛,3.阿里,4.minio
        // 如果是本地存储, 可以通过request构建出可直接访问的url
        MediaStoreTypeEnum storeTypeEnum = MediaStoreTypeEnum.valueOf(media.getStoreType());
        if (MediaStoreTypeEnum.LOCAL.equals(storeTypeEnum)) {
            // 文件访问路径
            media.setMediaPath(getMediaPath(request, media.getId()));
            // 文件名
            media.setName(media.getOriginalMediaName());
            // 带access__token的可直接访问路径
            media.setUrl(getMediaUrl(request, media.getId()));
        } else {
            media.setName(media.getOriginalMediaName());
            // 带access__token的可直接访问路径
            media.setUrl(getMediaUrl(request, media.getId().toString()));
        }
    }

    @Override
    public List<MediaVO> getListByIds(List<Long> ids) {
        return baseMapper.getListByIds(ids);
    }

    @Override
    public List<MediaVO> getListWithUrlByIds(final HttpServletRequest request, List<Long> ids) {
        return baseMapper.getListByIds(ids).stream()
                .peek(media -> genMediaVo(request, media))
                .collect(Collectors.toList());
    }

    @Override
    public Media saveFile(MultipartFile multipartFile, MediaStoreTypeEnum mediaStoreType) throws IOException {
        if (multipartFile == null || multipartFile.isEmpty()) {
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "上传文件不能为空");
        }
        String originalFilename = multipartFile.getOriginalFilename();
        // 转存为临时文件, 进行后续处理
        final File tmpFile = File.createTempFile(StringUtils.uuid(), null);
        multipartFile.transferTo(tmpFile);
        return saveFile(tmpFile, originalFilename, mediaStoreType);
    }

    @Override
    public Media saveFile(File file) throws FileNotFoundException {
        return saveFile(file, null, null);
    }

    @Override
    public Media saveFile(File file, String fileName, MediaStoreTypeEnum mediaStoreType) throws FileNotFoundException {
        checkFile(file);
        long fileSize = FileUtil.size(file);
        String originalFilename = StringUtils.isNotBlank(fileName) ? fileName : FileUtil.getName(file);
        originalFilename = FileUtil.cleanInvalid(originalFilename);
        String fileExtName = FilenameUtils.getExtension(originalFilename);
        // 如果上传文件后缀名不存在, 则根据文件流的头部信息获得文件类型
        if (StringUtils.isBlank(fileExtName)) {
            fileExtName = FileUtil.getType(file);
        }

        ////String mimeType1 = FileUtil.getMimeType(fileName);
        Map.Entry<String, String[]> mimeTypeEntry = FileUtils.getMimeTypeEntry(fileExtName);
        if (mimeTypeEntry == null) {
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "不支持的文件类型");
        }
        String mimeType = mimeTypeEntry.getKey();
        String[] type = mimeTypeEntry.getValue();
        final String extName = type[0];
        Integer mediaType = Integer.valueOf(type[1]);
        // 原上传文件文件名没有后缀时, 自动补全
        originalFilename = FileUtils.concatFilename(originalFilename, extName);
        // 生成新的文件名, 保证文件名不会重复
        String newFileName = FileUtils.concatFilename(StringUtils.uuid(), extName);
        MediaVO mediaVO;
        switch (mediaStoreType) {
            case QI_NIU:
                mediaVO = saveQiNiuOss(file, newFileName);
                break;
            case ALI:
                mediaVO = saveAliOss(file, newFileName);
                break;
            case MINIO:
                mediaVO = saveMinioOss(file, newFileName);
                break;
            default:
                mediaVO = saveLocal(file, newFileName);
                break;
        }
        return saveLocalMedia(originalFilename, newFileName, mediaVO.getMediaPath(), fileSize,
                mimeType, mediaVO.getStoreType(), mediaType, mediaVO.getAccessType(), mediaVO.getHashCode());
    }

    /**
     * 保存文件到本地
     *
     * @param file
     * @param newFileName
     * @return
     * @throws FileNotFoundException
     */
    private MediaVO saveLocal(File file, String newFileName) throws FileNotFoundException {
        Date now = new Date();
        // 文件所在dir路径
        String absolutePath = PathUtils.getDirAbsolutePath(uploadProperties.getUploadPath(), now);

        // 判断目录是否存在, 不存在则创建
        PathUtils.checkPathAndCreate(absolutePath);

        // 上传文件存储路径
        String filePath = PathUtils.concatPath(absolutePath, newFileName);
        File newFile = new File(filePath);
        try {
            // 复制文件到指定路径
            FileUtil.copy(file, newFile, true);
        } catch (IORuntimeException e) {
            log.error("文件保存失败", e);
            throw new RuntimeException("文件保存失败");
        }

        // 计算文件sha256 hash值
        String hashCode = FileUtils.checkSha256(newFile);

        // 生成文件存储的文件夹结构路径
        String relativeUrlPath = PathUtils.getRelativeUrlPath(uploadProperties.getUploadPath(), now);
        String mediaPath = relativeUrlPath + "/" + newFileName;
        return new MediaVO()
                .setHashCode(hashCode)
                .setMediaPath(mediaPath)
                .setAccessType(MediaAccessTypeEnum.PUBLIC.getValue())
                .setStoreType(MediaStoreTypeEnum.LOCAL.getValue());
    }

    /**
     * 保存文件到七牛oss
     *
     * @param file
     * @param newFileName
     * @return
     */
    private MediaVO saveQiNiuOss(File file, String newFileName) {
        DefaultPutRet putRet = qiNiuOssService.upload(file, newFileName);
        return new MediaVO()
                .setHashCode(putRet.hash)
                .setMediaPath(qiNiuOssProperties.getHost() + putRet.key)
                .setAccessType(qiNiuOssProperties.getAccessType().getValue())
                .setStoreType(MediaStoreTypeEnum.QI_NIU.getValue());
    }

    /**
     * 保存文件到阿里oss
     *
     * @param file
     * @param newFileName
     * @return
     */
    private MediaVO saveAliOss(File file, String newFileName) {
        PutObjectResult putObjectResult = aliOssService.upload(file, newFileName);
        String mediaPath = aliOssService.publicAccessUrl(newFileName);
        return new MediaVO()
                .setHashCode(putObjectResult.getETag())
                .setMediaPath(mediaPath)
                .setAccessType(aliOssProperties.getAccessType().getValue())
                .setStoreType(MediaStoreTypeEnum.ALI.getValue());
    }

    /**
     * 保存文件到minio oss
     *
     * @param file
     * @param newFileName
     * @return
     */
    private MediaVO saveMinioOss(File file, String newFileName) {
        ObjectWriteResponse objectWriteResponse = minioOssService.upload(file, newFileName);
        return new MediaVO()
                .setHashCode(objectWriteResponse.etag())
                .setMediaPath(minioOssProperties.getEndpoint() + "/" + minioOssProperties.getBucket() + "/" + objectWriteResponse.object())
                .setAccessType(minioOssProperties.getAccessType().getValue())
                .setStoreType(MediaStoreTypeEnum.MINIO.getValue());
    }

    /**
     * 校验文件的前置信息
     *
     * @param file
     */
    private void checkFile(File file) {
        if (!FileUtil.isFile(file)) {
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "文件不能为空");
        }
        if (FileUtil.isEmpty(file) || FileUtil.isSymlink(file)) {
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "不是有效的文件");
        }
        long maxFileSize = multipartProperties.getMaxFileSize().toBytes();
        long fileSize = FileUtil.size(file);
        if (fileSize > maxFileSize) {
            String maxReadableFileSize = FileUtil.readableFileSize(maxFileSize);
            // multipartProperties.getMaxFileSize().toMegabytes()
            String readableFileSize = FileUtil.readableFileSize(file);
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "上传文件为" + readableFileSize + ", 不能超过" + maxReadableFileSize);
        }
    }

    /**
     * 本地保存文件信息
     *
     * @param originalFilename 源文件名
     * @param newFilename      生成的uuid新文件名
     * @param relativePath     文件存储相对路径
     * @param fileSize         文件大小
     * @param mimeType         文件mime类型
     * @param storeType        文件mime类型
     * @param mediaType        资源类型(1.image,2.excel,3.word,4.pdf,5.zip)
     * @param accessType       资源类型(1.image,2.excel,3.word,4.pdf,5.zip)
     * @param hashCode         文件sha256校验值
     * @return
     */
    private Media saveLocalMedia(String originalFilename,
                                 String newFilename,
                                 String relativePath,
                                 long fileSize,
                                 String mimeType,
                                 int storeType,
                                 int mediaType,
                                 int accessType,
                                 String hashCode) {
        Media media = new Media()
                .setHashCode(StringUtils.defaultString(hashCode))
                .setOriginalMediaName(originalFilename)
                .setMediaName(newFilename)
                .setMediaPath(relativePath)
                .setMediaSize(fileSize)
                .setMimeType(mimeType)
                // 存储方式的类型(1.本地存储,2.七牛,3.阿里,4.minio)
                .setStoreType(storeType)
                .setMediaType(mediaType)
                .setDescription(originalFilename)
                // 访问类型(1.公开,2.私有,3.授权)
                .setAccessType(accessType);

        if (!save(media)) {
            throw new CysBusinessException(ErrorCodeEnum.ILLEGAL, "文件保存失败");
        }
        return media;
    }

    @Override
    public void download(HttpServletResponse response,
                         File file,
                         String fileName,
                         String contentType,
                         Boolean isAttachment,
                         Boolean isMsBrowser) throws IOException {
        // 先检测文件是否存在, 不存在则直接返回
        if (!FileUtil.exist(file)) {
            response.setContentType(CommonConstants.DEFAULT_CONTENT_TYPE);
            response.getWriter().write(objectMapper.writeValueAsString(illegal("文件不存在")));
            response.setStatus(404);
            response.flushBuffer();
            return;
        }
        // 如果isAttachment则默认不在Content-Disposition中写入attachment
        if (isAttachment == null) {
            isAttachment = false;
        }
        // 如果文件名没传, 则读取文件的名字
        if (StringUtils.isBlank(fileName)) {
            fileName = FileUtil.getName(file);
        }
        if (StringUtils.isBlank(contentType)) {
            contentType = FileUtil.getMimeType(file.getAbsolutePath());
            if (StringUtils.isBlank(contentType)) {
                contentType = "application/octet-stream";
            }
        }
        response.setCharacterEncoding(CommonConstants.DEFAULT_ENCODING);
        // 如果是ie浏览器, 对 content-type image/* 不设置响应头
        String imageContentTypePrefix = "image/";
        if (isMsBrowser != null && isMsBrowser && contentType.contains(imageContentTypePrefix)) {

        } else {
            response.setContentType(contentType);
        }
        long fileSize = FileUtil.size(file);
        response.setContentLengthLong(fileSize);
        // 文件名进行编码, 防止IE系列浏览器文件名乱码
        fileName = StringUtils.urlEncode(fileName);
        // 设置下载文件名, filename* 做兼容处理
        String contentDisposition = (isAttachment ? "attachment;" : "") +
                "filename=\"" + fileName + "\"; filename*=utf-8'zh_cn'" + fileName;
        response.setHeader("Content-Disposition", contentDisposition);
        // 如果要以下载的方式打开 需要设置
        ////response.setHeader("Content-Type", "application/octet-stream");
        ////response.setHeader(Content-Disposition", "attachment; filename="filename.ext");

        try (FileInputStream fis = new FileInputStream(file);
             ServletOutputStream out = response.getOutputStream()) {
            IOUtils.copy(fis, out);
            response.flushBuffer();
        } catch (Exception ex) {
            response.reset();
            response.setCharacterEncoding(CommonConstants.DEFAULT_ENCODING);
            response.setContentType(CommonConstants.DEFAULT_CONTENT_TYPE);
            response.getWriter().write(objectMapper.writeValueAsString(illegal("文件加载失败")));
            response.setStatus(404);
            response.flushBuffer();
            log.warn(ex.getMessage(), ex);
        }
    }

    @Override
    public void download(HttpServletResponse response,
                         File file,
                         String fileName,
                         String contentType) throws IOException {
        download(response, file, fileName, contentType, false, null);
    }

    @Override
    public void download(HttpServletResponse response, File file, String fileName) throws IOException {
        download(response, file, fileName, null);
    }

    @Override
    public void download(HttpServletResponse response, File file) throws IOException {
        download(response, file, null);
    }

    @Override
    public void download(HttpServletResponse response,
                         String filePath,
                         String fileName,
                         String contentType,
                         Boolean isAttachment) throws IOException {
        download(response, FileUtil.newFile(filePath), fileName, contentType, isAttachment, null);
    }

    @Override
    public void download(HttpServletResponse response,
                         String filePath,
                         String fileName,
                         String contentType) throws IOException {
        download(response, filePath, fileName, contentType, false);
    }

    @Override
    public void download(HttpServletResponse response,
                         String filePath,
                         String fileName,
                         String contentType,
                         Boolean isAttachment,
                         Boolean isMsBrowser) throws IOException {
        download(response, FileUtil.newFile(filePath), fileName, contentType, isAttachment, isMsBrowser);
    }

    @Override
    public void download(HttpServletResponse response, String filePath, String fileName) throws IOException {
        download(response, filePath, fileName, null);
    }

    @Override
    public void download(HttpServletResponse response, String filePath) throws IOException {
        download(response, filePath, null);
    }

    @Override
    public String getBaseUploadAbsolutePath() throws FileNotFoundException {
        return PathUtils.getBaseAbsolutePath(uploadProperties.getUploadPath());
    }

    @Override
    public String getUploadAbsolutePath(String mediaPath) throws FileNotFoundException {
        return PathUtils.concatPath(getBaseUploadAbsolutePath(), mediaPath);
    }

}
